#!/usr/bin/env python3
# -*- coding: utf-8 -*
# Name:     gen.py
# Purpose:  This 'general' module contain many functions required to be called at many places
# Authors:  Sundar
# Licence:  This file is a part of multibootusb package. You can redistribute it or modify
# under the terms of GNU General Public License, v.2 or above

import logging
import sys
import os
import platform
import shutil
import string
import zipfile
import re
import ctypes

from . import config
from .osdriver import get_physical_disk_number, wmi_get_drive_info, \
      log, resource_path, multibootusb_host_dir

def scripts_dir_path():
    return os.path.dirname(os.path.realpath(__file__))


def print_version():
    """
    Simple log the version number of the multibootusb application
    :return:
    """
    log('multibootusb version: ' + mbusb_version())


def quote(text):
    """
    Function to quote the input word or sentence.
    :param text:    Any word or sentence.
    :return:        Quoted text or sentence. If already quoted the same text is returned.
    """
    if not is_quoted(text):
        return '"' + text + '"'
    else:
        return text


def is_quoted(text):
    """
    Function to check if word is quoted.
    :param text:    Any word or sentence with or without quote.
    :return:        True if text is quoted else False.
    """
    return bool(text.startswith("\"") and text.endswith("\""))


def has_digit(word):
    """
    Useful function to detect if input word contain digit.
    :param word:    Any alphanumeric word.
    :return:        True if the word has a digit else False.
    """
    return any(char.isdigit() for char in word)


def sys_64bits():
    """
    Detect if the host system is 64 bit.
    :return:    True if system is 64 bit.
    """
    return sys.maxsize > 2**32


def iso_cfg_ext_dir():
    """
    Function to return the path to ISO configuration file extraction directory.
    :return:    Path to directory where ISO config files will be extracted.
    """
    return os.path.join(multibootusb_host_dir(), 'iso_cfg_ext_dir')


def clean_iso_cfg_ext_dir(iso_cfg_ext_dir):
    """
    Clean old ISO config files extracted by previous use of multibootusb.
    :param iso_cfg_ext_dir: Path to config extract directory.
    :return:
    """
    if os.path.exists(iso_cfg_ext_dir):
        for f in os.listdir(iso_cfg_ext_dir):
            fullpath = os.path.join(iso_cfg_ext_dir, f)
            if os.path.isdir(fullpath):
                shutil.rmtree(fullpath)
            else:
                os.remove(fullpath)
    else:
        log('iso_cfg_ext_dir directory does not exist.')


def copy_mbusb_dir_usb(usb_disk):
    """
    Copy the multibootusb directory to USB mount path.
    :param usb_mount_path: Path to USB mount.
    :return:
    """
#     from .iso import iso_size
    from .usb import details, PartitionNotMounted

    try:
        usb_details = details(usb_disk)
    except PartitionNotMounted as e:
        log(str(e))
        return False

    usb_mount_path = usb_details['mount_point']
    result = ''
    if not os.path.exists(os.path.join(usb_mount_path, "multibootusb")):
        try:
            log('Copying multibootusb directory to ' + usb_mount_path)
            shutil.copytree(resource_path(os.path.join("data", "multibootusb")), os.path.join(usb_mount_path, "multibootusb"))

            result = True
        except:
            log('multibootusb directory could not be copied to ' + usb_mount_path)
            result = False
    else:
        log('multibootusb directory already exists. Not copying.')

    if not os.path.exists(os.path.join(usb_mount_path, 'EFI', 'BOOT', 'multibootusb_grub2.txt')):
        if not os.path.exists(os.path.join(usb_mount_path, 'EFI', 'BOOT')):
            log('EFI/BOOT directory does not exist. Creating new.')
            os.makedirs(os.path.join(usb_mount_path, 'EFI', 'BOOT'), exist_ok=True)
        if os.path.exists(os.path.join(usb_mount_path, 'EFI')):
            shutil.rmtree(os.path.join(usb_mount_path, 'EFI'))
        try:
            log('Copying EFI directory to ' + usb_mount_path)
            shutil.copytree(resource_path(os.path.join("data", "EFI")), os.path.join(usb_mount_path, "EFI"))
            result = True
        except Exception as e:
            log(e)
            result = False
    else:
        log('EFI directory already exist. Not copying.')

    '''
    # For backward compatibility
    if not os.path.exists(os.path.join(usb_mount_path, 'EFI', 'BOOT', 'bootx64-gpt.efi')):
        shutil.copy(resource_path(os.path.join('data', 'EFI', 'BOOT', 'bootx64-gpt.efi')),
                    os.path.join(usb_mount_path, 'EFI', 'BOOT', 'bootx64-gpt.efi'))

    if not os.path.exists(os.path.join(usb_mount_path, 'EFI', 'BOOT', 'bootx64-msdos.efi')):
        shutil.copy(resource_path(os.path.join('data', 'EFI', 'BOOT', 'bootx64-msdos.efi')),
                    os.path.join(usb_mount_path, 'EFI', 'BOOT', 'bootx64-msdos.efi'))

    if not os.path.exists(os.path.join(usb_mount_path, 'multibootusb', 'grub', 'core-gpt.img')):
        shutil.copy(resource_path(os.path.join('data', 'multibootusb', 'grub', 'core-gpt.img')),
                    os.path.join(usb_mount_path, 'multibootusb', 'grub', 'core-gpt.img'))

    if not os.path.exists(os.path.join(usb_mount_path, 'multibootusb', 'grub', 'core-msdos.img')):
        shutil.copy(resource_path(os.path.join('data', 'multibootusb', 'grub', 'core-msdos.img')),
                    os.path.join(usb_mount_path, 'multibootusb', 'grub', 'core-msdos.img'))

    if not os.path.exists(os.path.join(usb_mount_path, 'multibootusb', 'grub', 'x86_64-efi')):
        log("New EFI modules does not exist. Copying now.")
        shutil.copytree(resource_path(os.path.join('data', 'multibootusb', 'grub', 'x86_64-efi')),
                    os.path.join(usb_mount_path, 'multibootusb', 'grub', 'x86_64-efi'))
        log("Replacing gpt efi binary with latest one.")
        os.remove(os.path.join(usb_mount_path, 'EFI', 'BOOT', 'bootx64-gpt.efi'))
        shutil.copy(resource_path(os.path.join('data', 'EFI', 'BOOT', 'bootx64-gpt.efi')),
                    os.path.join(usb_mount_path, 'EFI', 'BOOT', 'bootx64-gpt.efi'))
    '''
    # Warn users about older version
    if not os.path.exists(os.path.join(usb_mount_path, 'multibootusb', 'grub', 'x86_64-efi')):
        log("You have created live usb using old version of multibootusb.")
        log("Please remove EFI and multibootusb directory and reinstall distros again.")
    # Ensue that we have iso directory. Implemented from version 9.0.1
    if not os.path.exists(os.path.join(usb_mount_path, 'multibootusb', 'iso')):
        os.makedirs(os.path.join(usb_mount_path, 'multibootusb', 'iso'))

    # Update the menu files from resource path to USB directory.
    try:
        with zipfile.ZipFile(resource_path(os.path.join('data', 'multibootusb', 'grub', 'menus.zip')), "r") as z:
            z.extractall(os.path.join(usb_mount_path, 'multibootusb', 'grub', 'menus'))
    except:
        log('Unable to extract menu files to USB disk.')

    return result


def read_input_yes():
    """
    List option and read user input
    :return: True if user selected yes or else false
    """
    yes_list = ['Y', 'y', 'Yes', 'yes', 'YES']
    no_list = ['N', 'n', 'No', 'no', 'NO']
    response = input("Please enter the option listed above : ")
    if response in yes_list:
        return True
    elif response in no_list:
        return False


def strings(filename, _min=4):
    with open(filename, errors="ignore") as f:
        result = ""
        for c in f.read():
            if c in string.printable:
                result += c
                continue
            if len(result) >= _min:
                yield result
            result = ""
        if len(result) >= _min:  # catch result at EOF
            yield result


def size_not_enough(iso_link, usb_disk):
    from .iso import iso_size
    from .usb import details, PartitionNotMounted
    isoSize = iso_size(iso_link)
    try:
        usb_details = details(usb_disk)
    except PartitionNotMounted as e:
        log(str(e))
        return False
    usb_size = usb_details['size_free']

    return bool(isoSize > usb_size)


def mbusb_version():
    version = open(resource_path(os.path.join("data", "version.txt")), 'r').read().strip()
    return version


def check_text_in_file(file_path, text):
    """
    Helper function to check if a text exist in a file.
    :param file_path: Path to file
    :param text: Text to be searched
    :return: True if found else False
    """
    if not os.path.exists(file_path):
        return False
    else:
        with open(file_path) as data_file:
            return any(text in line for line in data_file)


def prepare_mbusb_host_dir():
    """
    Prepare multibootusb host directory and extract data files for use.
    :return:
    """
    home = multibootusb_host_dir()
    if not os.path.exists(home):
        os.makedirs(home)
    else:
        log("Cleaning old multibootusb directory...")
        clean_iso_cfg_ext_dir(os.path.join(home, "iso_cfg_ext_dir"))
        #shutil.rmtree(home)
        #os.makedirs(home)

    if not os.path.exists(os.path.join(home, "preference")):
        os.makedirs(os.path.join(home, "preference"))

    if not os.path.exists(os.path.join(home, "iso_cfg_ext_dir")):
        os.makedirs(os.path.join(home, "iso_cfg_ext_dir"))

    if os.path.exists(os.path.join(home, "syslinux", "bin", "syslinux4")):
        log("Syslinux exist in multibootusb directory...")
    else:
        log("Extracting syslinux to multibootusb directory...")
        if platform.system() == "Linux":
            if sys_64bits() is True:
                log('Host OS is 64 bit...')
                log("Extracting syslinux 64 bit...")
                # log(resource_path(os.path.join("data", "tools", "syslinux", "syslinux_linux_64.zip")))
                with zipfile.ZipFile(resource_path(os.path.join("data", "tools", "syslinux", "syslinux_linux_64.zip")), "r") as z:
                    z.extractall(home)
            else:
                log("Extracting syslinux 32 bit...")
                with zipfile.ZipFile(resource_path(os.path.join("data", "tools", "syslinux", "syslinux_linux.zip")), "r") as z:
                    z.extractall(home)
        else:
            with zipfile.ZipFile(resource_path(os.path.join("data", "tools", "syslinux", "syslinux_windows.zip")), "r") as z:
                z.extractall(home)
        log("Extracting syslinux modules to multibootusb directory...")
        with zipfile.ZipFile(resource_path(os.path.join("data", "tools", "syslinux", "syslinux_modules.zip")), "r") as z:
                z.extractall(os.path.join(home, "syslinux"))

    if os.listdir(os.path.join(home, "iso_cfg_ext_dir")):
        log(os.listdir(os.path.join(home, "iso_cfg_ext_dir")))
        log("iso extract directory is not empty.")
        log("Removing junk files...")
        for files in os.listdir(os.path.join(home, "iso_cfg_ext_dir")):
            if os.path.isdir(os.path.join(os.path.join(home, "iso_cfg_ext_dir", files))):
                log(os.path.join(os.path.join(home, "iso_cfg_ext_dir", files)))
                os.chmod(os.path.join(os.path.join(home, "iso_cfg_ext_dir", files)), 0o777)
                shutil.rmtree(os.path.join(os.path.join(home, "iso_cfg_ext_dir", files)))
            else:
                try:
                    log(os.path.join(os.path.join(home, "iso_cfg_ext_dir", files)))
                    os.chmod(os.path.join(os.path.join(home, "iso_cfg_ext_dir", files)), 0o777)
                    os.unlink(os.path.join(os.path.join(home, "iso_cfg_ext_dir", files)))
                    os.remove(os.path.join(os.path.join(home, "iso_cfg_ext_dir", files)))
                except OSError:
                    log("Can't remove the file. Skipping it.")


def grub_efi_exist(grub_efi_path):
    """
    Detect efi present in USB disk is copied by multibootusb.
    :param isolinux_path: Path to "grub efi image"
    :return: True if yes else False
    """
    from . import iso
    if grub_efi_path is not None:
        sl = list(iso.strings(grub_efi_path))
        for strin in sl:
            if re.search(r'multibootusb', strin, re.I):
                return True
        return False


def process_exist(process_name):
    """
    Detect if process exist/ running and kill it.
    :param process_name: process name to check
    :return: True if processis killed else False
    """
    if platform.system() == 'Windows':
        import signal
        import wmi
        c = wmi.WMI()
        for process in c.Win32_Process():
            if process_name in process.Name:
                log(process_name + ' exist...')
                log(str(process.ProcessId) + ' ' + str(process.Name))
                log("Having Windows explorer won't allow dd.exe to write ISO image properly."
                      "\nKilling the process..")
                try:
                    os.kill(process.ProcessId, signal.SIGTERM)
                    return True
                except:
                    log('Unable to kill process ' + str(process.ProcessId))

    return False


def write_to_file(filepath, text):
    """
    Simple function to write a text file
    :param filepath: Path to file
    :param text: Text to be written on to file
    :return:
    """
    with open(filepath, 'w') as f:
        f.write(text.strip())


class MemoryCheck():
    """
    Cross platform way to checks memory of a given system. Works on Linux and Windows.
    psutil is a good option to get memory info. But version 5.0 and only will work.
    Source: https://doeidoei.wordpress.com/2009/03/22/python-tip-3-checking-available-ram-with-python/
    Call this class like this:
    mem_info = memoryCheck()
    print(mem_info.value)
    """

    def __init__(self):

        if platform.system() == 'Linux':
            self.value = self.linuxRam()
        elif platform.system() == 'Windows':
            self.value = self.windowsRam()
        else:
            log("MemoryCheck Class will only work on Linux and Windows.")

    def windowsRam(self):
        """
        Uses Windows API to check RAM
        """
        kernel32 = ctypes.windll.kernel32
        c_ulong = ctypes.c_ulong

        class MEMORYSTATUS(ctypes.Structure):
            _fields_ = [
                ("dwLength", c_ulong),
                ("dwMemoryLoad", c_ulong),
                ("dwTotalPhys", c_ulong),
                ("dwAvailPhys", c_ulong),
                ("dwTotalPageFile", c_ulong),
                ("dwAvailPageFile", c_ulong),
                ("dwTotalVirtual", c_ulong),
                ("dwAvailVirtual", c_ulong)
            ]

        memoryStatus = MEMORYSTATUS()
        memoryStatus.dwLength = ctypes.sizeof(MEMORYSTATUS)
        kernel32.GlobalMemoryStatus(ctypes.byref(memoryStatus))

        return int(memoryStatus.dwTotalPhys / 1024 ** 2)

    def linuxRam(self):
        """
        Returns the RAM of a Linux system
        """
        totalMemory = os.popen("free -m").readlines()[1].split()[1]
        return int(totalMemory)

    
if __name__ == '__main__':
    log(quote("""Test-string"""))
    log(has_digit("test-string-with-01-digit"))
    log(sys_64bits())
    log(multibootusb_host_dir())
    log(iso_cfg_ext_dir())
    strings_test = strings('../../text-stings.bin')
    log(strings_test)
